Skip to content

feat: capture native app network through appium-interceptor in every adapter - #445

Merged
vishnuv688 merged 11 commits into
mainfrom
feat/444-native-network-interceptor
Oct 22, 2026
Merged

vishnuv688 merged 11 commits into
mainfrom
feat/444-native-network-interceptor

Conversation

@vishnuv688

@vishnuv688 vishnuv688 commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Closes #444

Why

A native Appium session has no document and Appium serves no BiDi network events, so the Network tab of a native app run is always empty. The page collector and BiDi, which cover every browser session, have nothing to attach to. The traffic is only visible to something sitting between the device and the network, and the appium-interceptor plugin is exactly that: it routes the device through a proxy and exposes what it recorded through interceptor: execute commands.

What

All four adapters read the plugin's proxy when the session asks for it with appium:intercept: true (top level or inside appium:options, flat or in alwaysMatch/firstMatch) and the session is native.

  • core (appium-interceptor.ts):
    • InterceptorNetworkPoller starts the listener (interceptor: startListening), reads interceptor: getInterceptedData and turns each entry it has not listed yet into a NetworkRequest. Header names are lowercased, the status line the plugin files as a header is dropped, and size is the body's byte length. Drains are chained so two overlapping reads cannot list the same entry, and a read that answers nothing usable is reported rather than dropped.
    • NativeNetworkCapture owns the lifecycle: gate, one listener per session, drains, stop. Each adapter supplies only an execute path that renders no action rows.
    • Every read returns the listener's whole history, so the drain after a command is throttled to once every 2 s (NATIVE_NETWORK_DRAIN_INTERVAL_MS). Drains at test end and session end always run.
    • SessionCapturer.recordNetworkRequests upserts rows through the capturer's index, so a re-sent id replaces its row.
  • service: raw WebDriver HTTP to the session. Drains after each command, before each test's trace slice is written, and at session end; a fresh listener on reloadSession.
  • selenium-devtools: the unpatched executeScript. The intercept flag is kept per driver, so concurrent builds do not share it. The per-test slice is chosen before the drain is awaited, so a fast next step cannot take it. Drains at quit, then stops.
  • nightwatch-devtools: raw WebDriver HTTP bound to the session and its configured default_path_prefix, through core's webdriverExecute. Drains after commands and at each test end (describe/it and cucumber), and stops the listener at a session rotation and at the end, so no read reaches a session that is gone.
  • selenium-devtools-py: interceptor_network.py mirrors the core logic, since the Python adapter ships no Node. Drains after commands, when each test reports its outcome and at quit. A replaced session's listener is dropped rather than read through the driver's new session.
  • docs: a "Native app network" section in each adapter README, examples/MOBILE.md and the CLAUDE.md network entry. Changesets: @wdio/devtools-service, @wdio/selenium-devtools and @wdio/nightwatch-devtools minor; Python changes/ fragment minor.

The plugin records no timing, so a row is stamped when it was first read and shows no duration. Requests the app makes before the listener starts (its launch traffic) are not listed.

Why is this in core

The same lifecycle lands in three JS adapters, so it lives in core and each adapter keeps only its execute path and drain points. It is plain HTTP to Appium and depends on no framework API. Python mirrors it because it cannot import TypeScript.

Scope

The plugin supports Android; HTTPS traffic needs an app that trusts the certificate the plugin installs.

Verification

  • pnpm test: 214 files, 2785 tests passed. Python suite: 759 tests OK.

  • pnpm build passes; pnpm lint 0 errors; pnpm typecheck clean; the dist leak grep over service, selenium-devtools and nightwatch-devtools is empty; changes.py check finds the fragment.

  • Regression tests fail without their fix: the per-test drain in the service, the Selenium slice race, the Python session replacement, the Nightwatch stop at rotation and the per-driver intercept flag in Selenium.

  • Not covered by a test: Nightwatch's stop in the plugin's after(), which has no test harness.

  • Checked on an Android emulator (Android 16, sdk_gphone64_arm64) with all four adapters, against a test app whose button issues a GET and a POST from Java code (no WebView): in each, the Network tab lists GET → 200 and POST → 201 once each as application/json, the Actions list carries only the test's own commands, and the requests appear in the trace archive. The app is not public, so that example is not committed. To repeat it with any app that makes HTTP calls, start Appium with the plugin and set appium:intercept: true:

    appium plugin install --source=npm appium-interceptor
    appium --use-plugins=appium-interceptor
  • Website docs: docs(devtools): Node.js 22.19 floor and native app network capture webdriverio#16176 mirrors the README sections on the MobileTesting page.

🤖 Generated with Claude Code

A native app has no document and Appium serves no BiDi network events,
so a native session's requests reached no Network tab. The
appium-interceptor plugin routes the device through a proxy that records
them; InterceptorNetworkPoller starts its listener and turns each new
entry into a NetworkRequest. It talks to the plugin over raw WebDriver
HTTP so its calls never re-enter an adapter's command hooks, and chains
drains so overlapping reads cannot list an entry twice.
…intercept

Starts the interceptor listener for a native session that asked for the
proxy, forwards what it recorded after each top-level command and at
session end, and re-arms it on reloadSession because a listener dies with
its session. Any other session, or Appium without the plugin, runs as
before; the latter logs a warning.
The service README says how to enable it and what a row carries,
MOBILE.md lists the remaining adapter gap, and CLAUDE.md's network entry
no longer says a native session has no network capture at all.
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no blocking issue remains from the supplied threads or the latest changes.

Summary

Adds native app network capture through appium-interceptor across all four adapters.

  • The WebdriverIO service lists native app requests in the Network view.
  • Selenium adds native app requests to Network results and traces.
  • Nightwatch records proxy requests for native Appium sessions.
  • Python Selenium records native app requests from the plugin proxy.

All three supplied, unnumbered review threads are addressed: the service drains before writing test traces, header names are lowercased, and Cucumber awaits the step’s drain and trace write. No new actionable issues were found in the changes since the previous review.

Diagram

sequenceDiagram
  participant Runner
  participant Adapter
  participant Proxy as Appium interceptor
  participant Capture
  participant Trace
  Runner->>Adapter: End test or step
  Adapter->>Proxy: Read recorded requests
  Proxy-->>Adapter: Listener history
  Adapter->>Capture: Add unseen requests
  Adapter->>Trace: Write test slice
  Adapter-->>Runner: Completion
  Runner->>Adapter: Start next test or step
Loading

Reviews (5) · Last reviewed commit: "fix(selenium-devtools): await each cucum..." · Reviewed by Greptile

Comment thread packages/service/src/index.ts
Comment thread packages/core/src/appium-interceptor.ts Outdated
The intercepted rows, like the two BiDi completion paths in the service's
session, were pushed straight onto networkRequests, bypassing the id index
the core capturer keeps. A request sent again under the same id was then
listed twice instead of replaced. recordNetworkRequests on the core
capturer upserts through that index and sends once, and all three sites
use it.

Also types the capabilities InterceptedNetwork reads instead of casting
from unknown, and moves the missing-browser guard on reload into start().
The proxy holds requests made after a test's last command until the
next drain, and the end-of-session drain ran after per-test traces
were already written, so those requests were missing from them.
Header names are lowercased so a mixed-case Content-Type still sets
the response type, and the capabilities parameter now accepts the
flat requested capabilities the tests and WDIO pass.
… share it

Selenium, Nightwatch and the WDIO service all need the same per-session
listener, drains and gating, so NativeNetworkCapture moves into core and
each adapter supplies only an execute path that renders no rows. The
listener also honours intercept inside appium:options, throttles the
per-command drain to once every 2 s because every read returns the whole
history, and reports a failed read instead of dropping it.
…terceptor

A native session's Network tab was always empty under Selenium. When the
build asks for appium:intercept, read the plugin's proxy after commands,
before each test's trace slice is written and at quit. The slice is
chosen before the drain is awaited, so a fast next step cannot take it,
and the intercept flag is kept per driver so concurrent builds do not
share it.
…interceptor

A native session's Network tab was always empty under Nightwatch. Read
the plugin's proxy over raw WebDriver HTTP, bound to the session and its
configured path prefix, after commands and at each test end, and stop
the listener at a session rotation and at the end so no read reaches a
session that is gone.
…-interceptor

A native app has no document and Appium serves no BiDi network events,
so a native session's Network tab was always empty. When the session
asks for appium:intercept, read the plugin's proxy after commands, when
each test reports its outcome and at quit, mapping each entry to the
same request row the JS adapters send. A replaced session's listener is
dropped rather than read through the driver's new session.
Each adapter README states the appium:intercept option, the plugin it
needs and when the proxy is read, and MOBILE.md and CLAUDE.md now say
all four adapters capture native network.
@vishnuv688 vishnuv688 changed the title feat(service): capture native app network through appium-interceptor feat: capture native app network through appium-interceptor in every adapter Oct 22, 2026
Comment thread packages/selenium-devtools/src/session-lifecycle.ts
…t starts

AfterStep did not wait for onTestEnd, so the next step recorded its trace
boundary while this step's native network drain was still in flight, and
requests read by that drain landed in the next step's slice.
@vishnuv688
vishnuv688 merged commit 0d78bfb into main Oct 22, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native app sessions capture no network requests

1 participant